import argparse
import getpass
import logging
import os
import sys
from typing import Optional

import cartography.config
import cartography.sync
import cartography.util
from cartography.intel.aws.util.common import parse_and_validate_aws_regions
from cartography.intel.aws.util.common import parse_and_validate_aws_requested_syncs
from cartography.intel.semgrep.dependencies import parse_and_validate_semgrep_ecosystems

logger = logging.getLogger(__name__)


class CLI:
    """
    :type sync: cartography.sync.Sync
    :param sync: A sync task for the command line program to execute.
    :type prog: string
    :param prog: The name of the command line program. This will be displayed in usage and help output.
    """

    def __init__(
        self,
        sync: Optional[cartography.sync.Sync] = None,
        prog: Optional[str] = None,
    ):
        self.sync = sync if sync else cartography.sync.build_default_sync()
        self.prog = prog
        self.parser = self._build_parser()

    def _build_parser(self):
        """
        :rtype: argparse.ArgumentParser
        :return: A cartography argument parser. Calling parse_args on the argument parser will return an object which
            implements the cartography.config.Config interface.
        """
        parser = argparse.ArgumentParser(
            prog=self.prog,
            description=(
                "cartography consolidates infrastructure assets and the relationships between them in an intuitive "
                "graph view. This application can be used to pull configuration data from multiple sources, load it "
                "in to Neo4j, and run arbitrary enrichment and analysis on that data. Please make sure you have Neo4j "
                "running and have configured AWS credentials with the SecurityAudit IAM policy before getting started. "
                "Running cartography with no parameters will execute a simple sync against a Neo4j instance running "
                "locally. It will use your default AWS credentials and will not execute and post-sync analysis jobs. "
                "Please see the per-parameter documentation below for information on how to connect to different Neo4j "
                "instances, use auth when communicating with Neo4j, sync data from multiple AWS accounts, and execute "
                "arbitrary analysis jobs after the conclusion of the sync."
            ),
            epilog="For more documentation please visit: https://github.com/cartography-cncf/cartography",
        )
        parser.add_argument(
            "-v",
            "--verbose",
            action="store_true",
            help="Enable verbose logging for cartography.",
        )
        parser.add_argument(
            "-q",
            "--quiet",
            action="store_true",
            help="Restrict cartography logging to warnings and errors only.",
        )
        parser.add_argument(
            "--neo4j-uri",
            type=str,
            default="bolt://localhost:7687",
            help=(
                "A valid Neo4j URI to sync against. See "
                "https://neo4j.com/docs/browser-manual/current/operations/dbms-connection/#uri-scheme for complete "
                "documentation on the structure of a Neo4j URI."
            ),
        )
        parser.add_argument(
            "--neo4j-user",
            type=str,
            default=None,
            help="A username with which to authenticate to Neo4j.",
        )
        parser.add_argument(
            "--neo4j-password-env-var",
            type=str,
            default=None,
            help="The name of an environment variable containing a password with which to authenticate to Neo4j.",
        )
        parser.add_argument(
            "--neo4j-password-prompt",
            action="store_true",
            help=(
                "Present an interactive prompt for a password with which to authenticate to Neo4j. This parameter "
                "supersedes other methods of supplying a Neo4j password."
            ),
        )
        parser.add_argument(
            "--neo4j-max-connection-lifetime",
            type=int,
            default=3600,
            help=(
                "Time in seconds for the Neo4j driver to consider a TCP connection alive. cartography default = 3600, "
                "which is the same as the Neo4j driver default. See "
                "https://neo4j.com/docs/driver-manual/1.7/client-applications/#driver-config-connection-pool-management"
                "."
            ),
        )
        parser.add_argument(
            "--neo4j-database",
            type=str,
            default=None,
            help=(
                "The name of the database in Neo4j to connect to. If not specified, uses the config settings of your "
                "Neo4j database itself to infer which database is set to default. "
                "See https://neo4j.com/docs/api/python-driver/4.4/api.html#database."
            ),
        )
        parser.add_argument(
            "--selected-modules",
            type=str,
            default=None,
            help=(
                'Comma-separated list of cartography top-level modules to sync. Example 1: "aws,gcp" to run AWS and GCP'
                "modules. See the full list available in source code at cartography.sync. "
                "If not specified, cartography by default will run all modules available and log warnings when it "
                "does not find credentials configured for them. "
                # TODO remove this mention about the create-indexes module when everything is using auto-indexes.
                "We recommend that you always specify the `create-indexes` module first in this list. "
                "If you specify the `analysis` module, we recommend that you include it as the LAST item of this list, "
                "(because it does not make sense to perform analysis on an empty/out-of-date graph)."
            ),
        )
        # TODO add the below parameters to a 'sync' subparser
        parser.add_argument(
            "--update-tag",
            type=int,
            default=None,
            help=(
                "A unique tag to apply to all Neo4j nodes and relationships created or updated during the sync run. "
                "This tag is used by cleanup jobs to identify nodes and relationships that are stale and need to be "
                "removed from the graph. By default, cartography will use a UNIX timestamp as the update tag."
            ),
        )
        parser.add_argument(
            "--aws-sync-all-profiles",
            action="store_true",
            help=(
                "Enable AWS sync for all discovered named profiles. When this parameter is supplied cartography will "
                "discover all configured AWS named profiles (see "
                "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) and run the AWS sync "
                'job for each profile not named "default". If this parameter is not supplied, cartography will use the '
                "default AWS credentials available in your environment to run the AWS sync once. When using this "
                "parameter it is suggested that you create an AWS config file containing a named profile for each AWS "
                "account you want to sync and use the AWS_CONFIG_FILE environment variable to point to that config "
                "file (see https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). cartography "
                "respects the AWS CLI/SDK environment variables and does not override them."
            ),
        )
        parser.add_argument(
            "--aws-regions",
            type=str,
            default=None,
            help=(
                '[EXPERIMENTAL!] Comma-separated list of AWS regions to sync. Example: specify "us-east-1,us-east-2" '
                "to sync US East 1 and 2. Note that this syncs the same regions in ALL accounts and it is currently "
                "not possible to specify different regions per account. "
                "CAUTION: if you previously synced assets from regions that are _not_ included in your current list, "
                "those assets will be _deleted_ during this sync. "
                'This is because cartography\'s cleanup process uses "lastupdated" and "account id" to determine data '
                "freshness and not regions. So, if a previously synced region is missing in the current sync, "
                "Cartography assumes the associated assets are stale and removes them. "
                "Default behavior: If `--aws-regions` is not specified, cartography will _autodiscover_ the "
                "regions supported by each account being synced."
            ),
        )
        parser.add_argument(
            "--aws-best-effort-mode",
            action="store_true",
            help=(
                "Enable AWS sync best effort mode when syncing AWS accounts. This will allow cartography to continue "
                "syncing other accounts and delay raising an exception until the very end."
            ),
        )
        parser.add_argument(
            "--aws-cloudtrail-management-events-lookback-hours",
            type=int,
            default=None,
            help=(
                "Number of hours back to retrieve CloudTrail management events from. If not specified, CloudTrail management events will not be retrieved."
            ),
        )
        parser.add_argument(
            "--oci-sync-all-profiles",
            action="store_true",
            help=(
                "Enable OCI sync for all discovered named profiles. When this parameter is supplied cartography will "
                "discover all configured OCI named profiles (see "
                "https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm) and run the OCI sync "
                'job for each profile not named "DEFAULT". If this parameter is not supplied, cartography will use the '
                "default OCI credentials available in your environment to run the OCI sync once."
            ),
        )
        parser.add_argument(
            "--azure-sync-all-subscriptions",
            action="store_true",
            help=(
                "Enable Azure sync for all discovered subscriptions. When this parameter is supplied cartography will "
                "discover all configured Azure subscriptions."
            ),
        )
        parser.add_argument(
            "--azure-sp-auth",
            action="store_true",
            help=("Use Service Principal authentication for Azure sync."),
        )
        parser.add_argument(
            "--azure-tenant-id",
            type=str,
            default=None,
            help=("Azure Tenant Id for Service Principal Authentication."),
        )
        parser.add_argument(
            "--azure-client-id",
            type=str,
            default=None,
            help=("Azure Client Id for Service Principal Authentication."),
        )
        parser.add_argument(
            "--azure-client-secret-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing Azure Client Secret for Service Principal Authentication."
            ),
        )
        parser.add_argument(
            "--azure-subscription-id",
            type=str,
            help="The Azure Subscription ID to sync.",
        )
        parser.add_argument(
            "--entra-tenant-id",
            type=str,
            default=None,
            help=("Entra Tenant Id for Service Principal Authentication."),
        )
        parser.add_argument(
            "--entra-client-id",
            type=str,
            default=None,
            help=("Entra Client Id for Service Principal Authentication."),
        )
        parser.add_argument(
            "--entra-client-secret-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing Entra Client Secret for Service Principal Authentication."
            ),
        )
        parser.add_argument(
            "--aws-requested-syncs",
            type=str,
            default=None,
            help=(
                'Comma-separated list of AWS resources to sync. Example 1: "ecr,s3,ec2:instance" for ECR, S3, and all '
                "EC2 instance resources. See the full list available in source code at cartography.intel.aws.resources."
                " If not specified, cartography by default will run all AWS sync modules available."
            ),
        )
        parser.add_argument(
            "--aws-guardduty-severity-threshold",
            type=str,
            default=None,
            help=(
                "GuardDuty severity threshold filter. Only findings at or above this severity level will be synced. "
                "Valid values: LOW, MEDIUM, HIGH, CRITICAL. If not specified, all findings (except archived) will be synced. "
                "Example: 'HIGH' will sync only HIGH and CRITICAL findings, filtering out LOW and MEDIUM severity findings."
            ),
        )
        parser.add_argument(
            "--experimental-aws-inspector-batch",
            type=int,
            default=1000,
            help=(
                "EXPERIMENTAL: This feature is experimental and may be removed in the future. "
                "Batch size for AWS Inspector findings sync. Controls how many findings are fetched, processed and cleaned up at a time. "
                "Default is 1000. Increase this value if you have a large number of findings and want to reduce API calls, "
                "or decrease it if you're experiencing memory issues."
            ),
        )
        parser.add_argument(
            "--analysis-job-directory",
            type=str,
            default=None,
            help=(
                "A path to a directory containing analysis jobs to run at the conclusion of the sync. cartography will "
                "discover all JSON files in the given directory (and its subdirectories) and pass them to the GraphJob "
                "API to execute against the graph. This allows you to apply data transformation and augmentation at "
                "the end of a sync run without writing code. cartography does not guarantee the order in which the "
                "jobs are executed."
            ),
        )
        parser.add_argument(
            "--okta-org-id",
            type=str,
            default=None,
            help=(
                "Okta organizational id to sync. Required if you are using the Okta intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--okta-api-key-env-var",
            type=str,
            default=None,
            help=(
                "The name of an environment variable containing a key with which to auth to the Okta API."
                "Required if you are using the Okta intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--okta-saml-role-regex",
            type=str,
            default=r"^aws\#\S+\#(?{{role}}[\w\-]+)\#(?{{accountid}}\d+)$",
            help=(
                "The regex used to map Okta groups to AWS roles when using okta as a SAML provider."
                "The regex is the one entered in Step 5: Enabling Group Based Role Mapping in Okta"
                "https://saml-doc.okta.com/SAML_Docs/How-to-Configure-SAML-2.0-for-Amazon-Web-Service#c-step5"
                "The regex must contain the {{role}} and {{accountid}} tags"
            ),
        )
        parser.add_argument(
            "--github-config-env-var",
            type=str,
            default=None,
            help=(
                "The name of an environment variable containing a Base64 encoded GitHub config object."
                "Required if you are using the GitHub intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--github-commit-lookback-days",
            type=int,
            default=30,
            help=(
                "Number of days to look back for tracking GitHub users committing to repositories. Defaults to 30 days."
            ),
        )
        parser.add_argument(
            "--digitalocean-token-env-var",
            type=str,
            default=None,
            help=(
                "The name of an environment variable containing a DigitalOcean access token."
                "Required if you are using the DigitalOcean intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--permission-relationships-file",
            type=str,
            default="cartography/data/permission_relationships.yaml",
            help=(
                "The path to the permission relationships mapping file."
                "If omitted the default permission relationships will be created"
            ),
        )
        parser.add_argument(
            "--azure-permission-relationships-file",
            type=str,
            default="cartography/data/azure_permission_relationships.yaml",
            help=(
                "The path to the Azure permission relationships mapping file."
                "If omitted the default Azure permission relationships will be created"
            ),
        )
        parser.add_argument(
            "--gcp-permission-relationships-file",
            type=str,
            default="cartography/data/gcp_permission_relationships.yaml",
            help=(
                "The path to the GCP permission relationships mapping file. "
                "If omitted the default GCP permission relationships will be used"
            ),
        )
        parser.add_argument(
            "--jamf-base-uri",
            type=str,
            default=None,
            help=(
                "Your Jamf base URI, e.g. https://hostname.com/JSSResource."
                "Required if you are using the Jamf intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--jamf-user",
            type=str,
            default=None,
            help="A username with which to authenticate to Jamf.",
        )
        parser.add_argument(
            "--jamf-password-env-var",
            type=str,
            default=None,
            help="The name of an environment variable containing a password with which to authenticate to Jamf.",
        )
        parser.add_argument(
            "--kandji-base-uri",
            type=str,
            default=None,
            help=(
                "Your Kandji base URI, e.g. https://company.api.kandji.io."
                "Required if you are using the Kandji intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--kandji-tenant-id",
            type=str,
            default=None,
            help=(
                "Your Kandji tenant id e.g. company."
                "Required using the Kandji intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--kandji-token-env-var",
            type=str,
            default=None,
            help="The name of an environment variable containing token with which to authenticate to Kandji.",
        )
        parser.add_argument(
            "--k8s-kubeconfig",
            default=None,
            type=str,
            help=(
                "The path to kubeconfig file specifying context to access K8s cluster(s)."
            ),
        )
        parser.add_argument(
            "--managed-kubernetes",
            default=None,
            type=str,
            help=("Type of managed Kubernetes service (e.g., 'eks'). Optional."),
        )
        parser.add_argument(
            "--nist-cve-url",
            type=str,
            default="https://services.nvd.nist.gov/rest/json/cves/2.0/",
            help=(
                "The base url for the NIST CVE data. Default = https://services.nvd.nist.gov/rest/json/cves/2.0/"
            ),
        )
        parser.add_argument(
            "--cve-enabled",
            action="store_true",
            help=("If set, CVE data will be synced from NIST."),
        )
        parser.add_argument(
            "--cve-api-key-env-var",
            type=str,
            default=None,
            help=("If set, uses the provided NIST NVD API v2.0 key."),
        )
        parser.add_argument(
            "--statsd-enabled",
            action="store_true",
            help=(
                "If set, enables sending metrics using statsd to a server of your choice."
            ),
        )
        parser.add_argument(
            "--statsd-prefix",
            type=str,
            default="",
            help=(
                "The string to prefix statsd metrics with. Only used if --statsd-enabled is on. Default = empty string."
            ),
        )
        parser.add_argument(
            "--statsd-host",
            type=str,
            default="127.0.0.1",
            help=(
                "The IP address of your statsd server. Only used if --statsd-enabled is on. Default = 127.0.0.1."
            ),
        )
        parser.add_argument(
            "--statsd-port",
            type=int,
            default=8125,
            help=(
                "The port of your statsd server. Only used if --statsd-enabled is on. Default = UDP 8125."
            ),
        )
        parser.add_argument(
            "--pagerduty-api-key-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing the pagerduty API key for authentication."
            ),
        )
        parser.add_argument(
            "--pagerduty-request-timeout",
            type=int,
            default=None,
            help=("Seconds to timeout for pagerduty API sessions."),
        )
        parser.add_argument(
            "--crowdstrike-client-id-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing the crowdstrike client id for authentication."
            ),
        )
        parser.add_argument(
            "--crowdstrike-client-secret-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing the crowdstrike secret key for authentication."
            ),
        )
        parser.add_argument(
            "--crowdstrike-api-url",
            type=str,
            default=None,
            help=(
                "The crowdstrike URL, if using self-hosted. Defaults to the public crowdstrike API URL otherwise."
            ),
        )
        parser.add_argument(
            "--gsuite-auth-method",
            type=str,
            default="delegated",
            choices=["delegated", "oauth", "default"],
            help=(
                'GSuite authentication method. Can be "delegated" for service account or "oauth" for OAuth. '
                '"Default" best if using gcloud CLI.'
            ),
        )
        parser.add_argument(
            "--gsuite-tokens-env-var",
            type=str,
            default="GSUITE_GOOGLE_APPLICATION_CREDENTIALS",
            help=(
                "The name of environment variable containing secrets for GSuite authentication."
            ),
        )
        parser.add_argument(
            "--googleworkspace-auth-method",
            type=str,
            default="delegated",
            choices=["delegated", "oauth", "default"],
            help=(
                'Google Workspace authentication method. Can be "delegated" for service account or "oauth" for OAuth. '
                '"Default" best if using gcloud CLI.'
            ),
        )
        parser.add_argument(
            "--googleworkspace-tokens-env-var",
            type=str,
            default="GOOGLEWORKSPACE_GOOGLE_APPLICATION_CREDENTIALS",
            help=(
                "The name of environment variable containing secrets for Google Workspace authentication."
            ),
        )
        parser.add_argument(
            "--lastpass-cid-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing the Lastpass CID for authentication."
            ),
        )
        parser.add_argument(
            "--lastpass-provhash-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing the Lastpass provhash for authentication."
            ),
        )
        parser.add_argument(
            "--bigfix-username",
            type=str,
            default=None,
            help=("The BigFix username for authentication."),
        )
        parser.add_argument(
            "--bigfix-password-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing the BigFix password for authentication."
            ),
        )
        parser.add_argument(
            "--bigfix-root-url",
            type=str,
            default=None,
            help=("The BigFix Root URL, a.k.a the BigFix API URL"),
        )
        parser.add_argument(
            "--duo-api-key-env-var",
            type=str,
            default=None,
            help=("The name of environment variable containing the Duo api key"),
        )
        parser.add_argument(
            "--duo-api-secret-env-var",
            type=str,
            default=None,
            help=("The name of environment variable containing the Duo api secret"),
        )
        parser.add_argument(
            "--duo-api-hostname",
            type=str,
            default=None,
            help=("The Duo api hostname"),
        )
        parser.add_argument(
            "--semgrep-app-token-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing the Semgrep app token key. "
                "Required if you are using the Semgrep intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--semgrep-dependency-ecosystems",
            type=str,
            default=None,
            help=(
                "Comma-separated list of language ecosystems for which dependencies will be retrieved from Semgrep. "
                'For example, a value of "gomod,npm" will retrieve Go and NPM dependencies. '
                "See the full list of supported ecosystems in source code at cartography.intel.semgrep.dependencies. "
                "Required if you are using the Semgrep dependencies intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--snipeit-base-uri",
            type=str,
            default=None,
            help=(
                "Your SnipeIT base URI. "
                "Required if you are using the SnipeIT intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--snipeit-token-env-var",
            type=str,
            default=None,
            help="The name of an environment variable containing token with which to authenticate to SnipeIT.",
        )
        parser.add_argument(
            "--snipeit-tenant-id",
            type=str,
            default=None,
            help="An ID for the SnipeIT tenant.",
        )
        parser.add_argument(
            "--cloudflare-token-env-var",
            type=str,
            default=None,
            help="The name of an environment variable containing ApiKey with which to authenticate to Cloudflare.",
        )
        parser.add_argument(
            "--tailscale-token-env-var",
            type=str,
            default=None,
            help=(
                "The name of an environment variable containing a Tailscale API token. "
                "Required if you are using the Tailscale intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--tailscale-org",
            type=str,
            default=None,
            help=(
                "The name of the Tailscale organization to sync. "
                "Required if you are using the Tailscale intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--tailscale-base-url",
            type=str,
            default="https://api.tailscale.com/api/v2",
            help=(
                "The base URL for the Tailscale API. "
                "Required if you are using the Tailscale intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--openai-apikey-env-var",
            type=str,
            default=None,
            help=(
                "The name of an environment variable containing a OpenAI API Key. "
                "Required if you are using the OpenAI intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--openai-org-id",
            type=str,
            default=None,
            help=(
                "The ID of the OpenAI organization to sync. "
                "Required if you are using the OpenAI intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--anthropic-apikey-env-var",
            type=str,
            default=None,
            help=(
                "The name of an environment variable containing an Anthropic API Key. "
                "Required if you are using the Anthropic intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--airbyte-client-id",
            type=str,
            default=None,
            help=(
                "The Airbyte client ID to use for authentication. "
                "Required if you are using the Airbyte intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--airbyte-client-secret-env-var",
            type=str,
            default=None,
            help=(
                "The name of an environment variable containing the Airbyte client secret for authentication. "
                "Required if you are using the Airbyte intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--airbyte-api-url",
            type=str,
            default="https://api.airbyte.com/v1",
            help=(
                "The base URL for the Airbyte API (default is the public Airbyte Cloud API). "
                "Required if you are using the Airbyte intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--trivy-s3-bucket",
            type=str,
            default=None,
            help=(
                "The S3 bucket name containing Trivy scan results. "
                "Required if you are using the Trivy module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--trivy-s3-prefix",
            type=str,
            default=None,
            help=(
                "The S3 prefix path containing Trivy scan results. "
                "Required if you are using the Trivy module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--ontology-users-source",
            type=str,
            default=None,
            help=(
                "Comma-separated list of sources of truth for user data in the ontology. "
                "'User' nodes will only be created for users that exist in one of the sources. "
                "Required if you are using the ontology module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--ontology-devices-source",
            type=str,
            default=None,
            help=(
                "Comma-separated list of sources of truth for client computer data in the ontology. "
                "'Device' nodes will only be created for groups that exist in one of the sources. "
                "Required if you are using the ontology module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--trivy-results-dir",
            type=str,
            default=None,
            help=(
                "Path to a directory containing Trivy JSON results on disk. "
                "Required if you are using the Trivy module with local results."
            ),
        )
        parser.add_argument(
            "--scaleway-org",
            type=str,
            default=None,
            help=(
                "The Scaleway organization ID to sync. "
                "Required if you are using the Scaleway intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--scaleway-access-key",
            type=str,
            default=None,
            help=(
                "The Scaleway access key to use for authentication. "
                "Required if you are using the Scaleway intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--scaleway-secret-key-env-var",
            type=str,
            default=None,
            help=(
                "The name of an environment variable containing the Scaleway secret key for authentication. "
                "Required if you are using the Scaleway intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--sentinelone-account-ids",
            type=str,
            default=None,
            help=(
                "Comma-separated list of SentinelOne account IDs to sync. "
                "If not specified, all accessible accounts will be synced."
            ),
        )
        parser.add_argument(
            "--sentinelone-api-url",
            type=str,
            default=None,
            help=(
                "SentinelOne API URL. Required if you are using the SentinelOne intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--sentinelone-api-token-env-var",
            type=str,
            default="SENTINELONE_API_TOKEN",
            help=(
                "The name of an environment variable containing the SentinelOne API token. "
                "Required if you are using the SentinelOne intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--keycloak-client-id",
            type=str,
            default=None,
            help=(
                "The Keycloak client ID to sync. "
                "Required if you are using the Keycloak intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--keycloak-client-secret-env-var",
            type=str,
            default="KEYCLOAK_CLIENT_SECRET",
            help=(
                "The name of an environment variable containing the Keycloak client secret. "
                "Required if you are using the Keycloak intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--keycloak-url",
            type=str,
            help=(
                "The base URL for the Keycloak instance. "
                "Required if you are using the Keycloak intel module. Ignored otherwise. "
            ),
        )
        parser.add_argument(
            "--keycloak-realm",
            type=str,
            default="master",
            help=(
                "The Keycloak realm used for authentication (note: all available realms will be synced). "
                "Should be `master` (default value) in most of the cases. "
                "Required if you are using the Keycloak intel module. Ignored otherwise. "
            ),
        )
        parser.add_argument(
            "--slack-token-env-var",
            type=str,
            default=None,
            help=(
                "The name of environment variable containing the Slack Token. "
                "Required if you are using the Slack intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--slack-teams",
            type=str,
            default=None,
            help=(
                "The Slack Team ID to sync, comma separated. If not provided, all accessible teams will be synced. "
            ),
        )
        parser.add_argument(
            "--slack-channels-memberships",
            action="store_true",
            help=("Pull memberships for Slack Channels (can be time consuming)."),
        )
        parser.add_argument(
            "--spacelift-api-endpoint",
            type=str,
            default=None,
            help=(
                "Spacelift GraphQL API endpoint (e.g., https://yourorg.app.spacelift.io/graphql). "
                "Required if you are using the Spacelift intel module. Ignored otherwise."
            ),
        )
        parser.add_argument(
            "--spacelift-api-token-env-var",
            type=str,
            default="SPACELIFT_API_TOKEN",
            help=(
                "The name of an environment variable containing the Spacelift API token. "
                "Alternative to using API key ID/secret. Ignored if API key credentials are provided."
            ),
        )
        parser.add_argument(
            "--spacelift-api-key-id-env-var",
            type=str,
            default="SPACELIFT_API_KEY_ID",
            help=(
                "The name of an environment variable containing the Spacelift API key ID. "
                "Use with --spacelift-api-key-secret-env-var for automatic token exchange. "
                "Alternative to providing a pre-generated token."
            ),
        )
        parser.add_argument(
            "--spacelift-api-key-secret-env-var",
            type=str,
            default="SPACELIFT_API_KEY_SECRET",
            help=(
                "The name of an environment variable containing the Spacelift API key secret. "
                "Use with --spacelift-api-key-id-env-var for automatic token exchange. "
                "Alternative to providing a pre-generated token."
            ),
        )
        parser.add_argument(
            "--spacelift-ec2-ownership-aws-profile",
            type=str,
            default=None,
            help=(
                "AWS profile name to use for fetching EC2 ownership data from S3. "
                "Optional. If not provided, uses default AWS credentials. "
            ),
        )
        parser.add_argument(
            "--spacelift-ec2-ownership-s3-bucket",
            type=str,
            default=None,
            help=(
                "S3 bucket name containing CloudTrail data for EC2 ownership relationships. "
                "Required for EC2 ownership sync (along with --spacelift-ec2-ownership-s3-prefix)."
            ),
        )
        parser.add_argument(
            "--spacelift-ec2-ownership-s3-prefix",
            type=str,
            default=None,
            help=(
                "S3 prefix for CloudTrail data for EC2 ownership relationships. "
                "All JSON files under this prefix will be processed. "
                "Required for EC2 ownership sync (along with --spacelift-ec2-ownership-s3-bucket)."
            ),
        )

        return parser

    def main(self, argv: str) -> int:
        """
        Entrypoint for the command line interface.

        :type argv: string
        :param argv: The parameters supplied to the command line program.
        """
        # TODO support parameter lookup in environment variables if not present on command line
        config: argparse.Namespace = self.parser.parse_args(argv)
        # Logging config
        if config.verbose:
            logging.getLogger("cartography").setLevel(logging.DEBUG)
        elif config.quiet:
            logging.getLogger("cartography").setLevel(logging.WARNING)
        else:
            logging.getLogger("cartography").setLevel(logging.INFO)
        logger.debug("Launching cartography with CLI configuration: %r", vars(config))
        # Neo4j config
        if config.neo4j_user:
            config.neo4j_password = None
            if config.neo4j_password_prompt:
                logger.info(
                    "Reading password for Neo4j user '%s' interactively.",
                    config.neo4j_user,
                )
                config.neo4j_password = getpass.getpass()
            elif config.neo4j_password_env_var:
                logger.debug(
                    "Reading password for Neo4j user '%s' from environment variable '%s'.",
                    config.neo4j_user,
                    config.neo4j_password_env_var,
                )
                config.neo4j_password = os.environ.get(config.neo4j_password_env_var)
            if not config.neo4j_password:
                logger.warning(
                    "Neo4j username was provided but a password could not be found.",
                )
        else:
            config.neo4j_password = None

        # Selected modules
        if config.selected_modules:
            self.sync = cartography.sync.build_sync(config.selected_modules)

        # AWS config
        if config.aws_requested_syncs:
            # No need to store the returned value; we're using this for input validation.
            parse_and_validate_aws_requested_syncs(config.aws_requested_syncs)

        # AWS regions
        if config.aws_regions:
            # No need to store the returned value; we're using this for input validation.
            parse_and_validate_aws_regions(config.aws_regions)

        # Azure config
        if config.azure_sp_auth and config.azure_client_secret_env_var:
            logger.debug(
                "Reading Client Secret for Azure Service Principal Authentication from environment variable %s",
                config.azure_client_secret_env_var,
            )
            config.azure_client_secret = os.environ.get(
                config.azure_client_secret_env_var,
            )
        else:
            config.azure_client_secret = None

        # Entra config
        if (
            config.entra_tenant_id
            and config.entra_client_id
            and config.entra_client_secret_env_var
        ):
            logger.debug(
                "Reading Client Secret for Entra Authentication from environment variable %s",
                config.entra_client_secret_env_var,
            )
            config.entra_client_secret = os.environ.get(
                config.entra_client_secret_env_var
            )
        else:
            config.entra_client_secret = None

        # Okta config
        if config.okta_org_id and config.okta_api_key_env_var:
            logger.debug(
                f"Reading API key for Okta from environment variable {config.okta_api_key_env_var}",
            )
            config.okta_api_key = os.environ.get(config.okta_api_key_env_var)
        else:
            config.okta_api_key = None

        # GitHub config
        if config.github_config_env_var:
            logger.debug(
                f"Reading config string for GitHub from environment variable {config.github_config_env_var}",
            )
            config.github_config = os.environ.get(config.github_config_env_var)
        else:
            config.github_config = None

        # DigitalOcean config
        if config.digitalocean_token_env_var:
            logger.debug(
                f"Reading token for DigitalOcean from env variable {config.digitalocean_token_env_var}",
            )
            config.digitalocean_token = os.environ.get(
                config.digitalocean_token_env_var,
            )
        else:
            config.digitalocean_token = None

        # Jamf config
        if config.jamf_base_uri:
            if config.jamf_user:
                config.jamf_password = None
                if config.jamf_password_env_var:
                    logger.debug(
                        "Reading password for Jamf user '%s' from environment variable '%s'.",
                        config.jamf_user,
                        config.jamf_password_env_var,
                    )
                    config.jamf_password = os.environ.get(config.jamf_password_env_var)
            if not config.jamf_user:
                logger.warning("A Jamf base URI was provided but a user was not.")
            if not config.jamf_password:
                logger.warning("A Jamf password could not be found.")
        else:
            config.jamf_user = None
            config.jamf_password = None

        # Kandji config
        if config.kandji_base_uri:
            if config.kandji_token_env_var:
                logger.debug(
                    "Reading Kandji API token from environment variable '%s'.",
                    config.kandji_token_env_var,
                )
                config.kandji_token = os.environ.get(config.kandji_token_env_var)
            elif os.environ.get("KANDJI_TOKEN"):
                logger.debug(
                    "Reading Kandji API token from environment variable 'KANDJI_TOKEN'.",
                )
                config.kandji_token = os.environ.get("KANDJI_TOKEN")
            else:
                logger.warning("A Kandji base URI was provided but a token was not.")
                config.kandji_token = None
        else:
            config.kandji_base_uri = None
            config.kandji_token = None

        if config.statsd_enabled:
            logger.debug(
                f"statsd enabled. Sending metrics to server {config.statsd_host}:{config.statsd_port}. "
                f'Metrics have prefix "{config.statsd_prefix}".',
            )

        # Pagerduty config
        if config.pagerduty_api_key_env_var:
            logger.debug(
                f"Reading API key for PagerDuty from environment variable {config.pagerduty_api_key_env_var}",
            )
            config.pagerduty_api_key = os.environ.get(config.pagerduty_api_key_env_var)
        else:
            config.pagerduty_api_key = None

        # Crowdstrike config
        if config.crowdstrike_client_id_env_var:
            logger.debug(
                f"Reading API key for Crowdstrike from environment variable {config.crowdstrike_client_id_env_var}",
            )
            config.crowdstrike_client_id = os.environ.get(
                config.crowdstrike_client_id_env_var,
            )
        else:
            config.crowdstrike_client_id = None

        if config.crowdstrike_client_secret_env_var:
            logger.debug(
                f"Reading API key for Crowdstrike from environment variable {config.crowdstrike_client_secret_env_var}",
            )
            config.crowdstrike_client_secret = os.environ.get(
                config.crowdstrike_client_secret_env_var,
            )
        else:
            config.crowdstrike_client_secret = None

        # GSuite config
        if config.gsuite_tokens_env_var:
            logger.debug(
                f"Reading config string for GSuite from environment variable {config.gsuite_tokens_env_var}",
            )
            config.gsuite_config = os.environ.get(config.gsuite_tokens_env_var)
        else:
            config.gsuite_tokens_env_var = None

        # Google Workspace config
        if config.googleworkspace_tokens_env_var:
            logger.debug(
                f"Reading config string for Google Workspace from environment variable {config.googleworkspace_tokens_env_var}",
            )
            config.googleworkspace_config = os.environ.get(
                config.googleworkspace_tokens_env_var
            )
        else:
            config.googleworkspace_tokens_env_var = None

        # Lastpass config
        if config.lastpass_cid_env_var:
            logger.debug(
                f"Reading CID for Lastpass from environment variable {config.lastpass_cid_env_var}",
            )
            config.lastpass_cid = os.environ.get(config.lastpass_cid_env_var)
        else:
            config.lastpass_cid = None
        if config.lastpass_provhash_env_var:
            logger.debug(
                f"Reading provhash for Lastpass from environment variable {config.lastpass_provhash_env_var}",
            )
            config.lastpass_provhash = os.environ.get(config.lastpass_provhash_env_var)
        else:
            config.lastpass_provhash = None

        # BigFix config
        if (
            config.bigfix_username
            and config.bigfix_password_env_var
            and config.bigfix_root_url
        ):
            logger.debug(
                f"Reading BigFix password from environment variable {config.bigfix_password_env_var}",
            )
            config.bigfix_password = os.environ.get(config.bigfix_password_env_var)

        # Duo config
        if (
            config.duo_api_key_env_var
            and config.duo_api_secret_env_var
            and config.duo_api_hostname
        ):
            logger.debug(
                f"Reading Duo api key and secret from environment variables {config.duo_api_key_env_var}"
                f", {config.duo_api_secret_env_var}",
            )
            config.duo_api_key = os.environ.get(config.duo_api_key_env_var)
            config.duo_api_secret = os.environ.get(config.duo_api_secret_env_var)
        else:
            config.duo_api_key = None
            config.duo_api_secret = None

        # Semgrep config
        if config.semgrep_app_token_env_var:
            logger.debug(
                f"Reading Semgrep App Token from environment variable {config.semgrep_app_token_env_var}",
            )
            config.semgrep_app_token = os.environ.get(config.semgrep_app_token_env_var)
        else:
            config.semgrep_app_token = None
        if config.semgrep_dependency_ecosystems:
            # No need to store the returned value; we're using this for input validation.
            parse_and_validate_semgrep_ecosystems(config.semgrep_dependency_ecosystems)

        # CVE feed config
        if config.cve_api_key_env_var:
            logger.debug(
                f"Reading NVD CVE API key environment variable {config.cve_api_key_env_var}",
            )
            config.cve_api_key = os.environ.get(config.cve_api_key_env_var)
        else:
            config.cve_api_key = None

        # SnipeIT config
        if config.snipeit_base_uri:
            if config.snipeit_token_env_var:
                logger.debug(
                    "Reading SnipeIT API token from environment variable '%s'.",
                    config.snipeit_token_env_var,
                )
                config.snipeit_token = os.environ.get(config.snipeit_token_env_var)
            elif os.environ.get("SNIPEIT_TOKEN"):
                logger.debug(
                    "Reading SnipeIT API token from environment variable 'SNIPEIT_TOKEN'.",
                )
                config.snipeit_token = os.environ.get("SNIPEIT_TOKEN")
            else:
                logger.warning("A SnipeIT base URI was provided but a token was not.")
                config.snipeit_token = None
        else:
            config.snipeit_base_uri = None
            config.snipeit_token = None

        # Tailscale config
        if config.tailscale_token_env_var:
            logger.debug(
                f"Reading Tailscale API token from environment variable {config.tailscale_token_env_var}",
            )
            config.tailscale_token = os.environ.get(config.tailscale_token_env_var)
        else:
            config.tailscale_token = None

        # Cloudflare config
        if config.cloudflare_token_env_var:
            logger.debug(
                f"Reading Cloudflare ApiKey from environment variable {config.cloudflare_token_env_var}",
            )
            config.cloudflare_token = os.environ.get(config.cloudflare_token_env_var)
        else:
            config.cloudflare_token = None

        # OpenAI config
        if config.openai_apikey_env_var:
            logger.debug(
                f"Reading OpenAI API key from environment variable {config.openai_apikey_env_var}",
            )
            config.openai_apikey = os.environ.get(config.openai_apikey_env_var)
        else:
            config.openai_apikey = None

        # Anthropic config
        if config.anthropic_apikey_env_var:
            logger.debug(
                f"Reading Anthropic API key from environment variable {config.anthropic_apikey_env_var}",
            )
            config.anthropic_apikey = os.environ.get(config.anthropic_apikey_env_var)
        else:
            config.anthropic_apikey = None

        # Airbyte config
        if config.airbyte_client_id and config.airbyte_client_secret_env_var:
            logger.debug(
                f"Reading Airbyte client secret from environment variable {config.airbyte_client_secret_env_var}",
            )
            config.airbyte_client_secret = os.environ.get(
                config.airbyte_client_secret_env_var,
            )
        else:
            config.airbyte_client_secret = None

        # Trivy config
        if config.trivy_s3_bucket:
            logger.debug(f"Trivy S3 bucket: {config.trivy_s3_bucket}")

        if config.trivy_s3_prefix:
            logger.debug(f"Trivy S3 prefix: {config.trivy_s3_prefix}")

        if config.trivy_results_dir:
            logger.debug(f"Trivy results dir: {config.trivy_results_dir}")

        # Scaleway config
        if config.scaleway_secret_key_env_var:
            logger.debug(
                f"Reading Scaleway secret key from environment variable {config.scaleway_secret_key_env_var}",
            )
            config.scaleway_secret_key = os.environ.get(
                config.scaleway_secret_key_env_var,
            )
        else:
            config.scaleway_secret_key = None

        # SentinelOne config
        if config.sentinelone_account_ids:
            config.sentinelone_account_ids = [
                id.strip() for id in config.sentinelone_account_ids.split(",")
            ]
            logger.debug(
                f"Parsed {len(config.sentinelone_account_ids)} SentinelOne account IDs to sync"
            )
        else:
            config.sentinelone_account_ids = None

        if config.sentinelone_api_url and config.sentinelone_api_token_env_var:
            logger.debug(
                f"Reading API token for SentinelOne from environment variable {config.sentinelone_api_token_env_var}",
            )
            config.sentinelone_api_token = os.environ.get(
                config.sentinelone_api_token_env_var
            )
        else:
            config.sentinelone_api_token = None

        # Keycloak config
        if config.keycloak_client_secret_env_var:
            logger.debug(
                f"Reading Client Secret for Keycloak from environment variable {config.keycloak_client_secret_env_var}",
            )
            config.keycloak_client_secret = os.environ.get(
                config.keycloak_client_secret_env_var
            )
        else:
            config.keycloak_client_secret = None

        # Slack config
        if config.slack_token_env_var:
            logger.debug(
                f"Reading Slack token from environment variable {config.slack_token_env_var}",
            )
            config.slack_token = os.environ.get(config.slack_token_env_var)
        else:
            config.slack_token = None

        # Spacelift config
        # Read endpoint from CLI arg or env var
        if not config.spacelift_api_endpoint:
            config.spacelift_api_endpoint = os.environ.get("SPACELIFT_API_ENDPOINT")

        if config.spacelift_api_endpoint:
            # Try to read API token
            if config.spacelift_api_token_env_var:
                logger.debug(
                    f"Reading API token for Spacelift from environment variable {config.spacelift_api_token_env_var}",
                )
                config.spacelift_api_token = os.environ.get(
                    config.spacelift_api_token_env_var
                )
            else:
                config.spacelift_api_token = None

            # Try to read API key ID and secret
            if config.spacelift_api_key_id_env_var:
                logger.debug(
                    f"Reading API key ID for Spacelift from environment variable {config.spacelift_api_key_id_env_var}",
                )
                config.spacelift_api_key_id = os.environ.get(
                    config.spacelift_api_key_id_env_var
                )
            else:
                config.spacelift_api_key_id = None

            if config.spacelift_api_key_secret_env_var:
                logger.debug(
                    f"Reading API key secret for Spacelift from environment variable {config.spacelift_api_key_secret_env_var}",
                )
                config.spacelift_api_key_secret = os.environ.get(
                    config.spacelift_api_key_secret_env_var
                )
            else:
                config.spacelift_api_key_secret = None
        else:
            config.spacelift_api_token = None
            config.spacelift_api_key_id = None
            config.spacelift_api_key_secret = None

        # Run cartography
        try:
            return cartography.sync.run_with_config(self.sync, config)
        except KeyboardInterrupt:
            return cartography.util.STATUS_KEYBOARD_INTERRUPT


def main(argv=None):
    """
    Entrypoint for the default cartography command line interface.

    This entrypoint build and executed the default cartography sync. See cartography.sync.build_default_sync.

    :rtype: int
    :return: The return code.
    """
    logging.basicConfig(level=logging.INFO)
    logging.getLogger("botocore").setLevel(logging.WARNING)
    logging.getLogger("googleapiclient").setLevel(logging.WARNING)
    logging.getLogger("neo4j").setLevel(logging.WARNING)
    logging.getLogger("azure.identity").setLevel(logging.WARNING)
    logging.getLogger("httpx").setLevel(logging.WARNING)
    logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(
        logging.WARNING
    )

    argv = argv if argv is not None else sys.argv[1:]
    sys.exit(CLI(prog="cartography").main(argv))
